/**
 * @file      filetransfer.c
 * @author    wzzlyzdn (wzzlyzdn@163.com)
 * @brief     文件传输函数封装
 * @version   0.1
 * @date      2022-08-06
 *
 * @copyright Copyright (c) 2022 wzzlyzdn
 *
 * @note      历史记录:
 *            - 创建初始版本
 *            - 增加文件传输完毕 MD5 校验
 * @warning
 * @par       修改记录:
 * <table>
 * <tr><th>date          <th>Version    <th>Author      <th>Description           </tr>
 * <tr><td>2022-08-06    <td> 0.1       <td>wzzlyzdn       <td>创建初始版本            </tr>
 * <tr><td>2022-08-12    <td> 0.2       <td>wzzlyzdn       <td>增加代码注释            </tr>
 * <tr><td>2022-08-12    <td> 0.2       <td>wzzlyzdn       <td>增加文件传输 MD5 校验    </tr>
 * <tr><td>2022-08-15    <td> 0.3       <td>wzzlyzdn       <td>入参，返回值校验         </tr>
 * </table>
 */

#include "server_client.h"
#include "server.h"
#include "md5sum.h"
#include <string.h>
#include <dirent.h>

static int file_transfor_flag = 0;
static int file_receive_flag = 0;

int file_transfer_state = FILE_TRANSFER;

/**
 * @fn        void display_flie_list(char *buf, int sockfd)
 * @brief     打印文件列表
 *
 * @param     [in,out] buf       将文件列表传入 buf
 * @param     [in] sockfd        发送目的地 sockfd
 *
 */
void display_flie_list(char *buf, int sockfd)
{
    int i = 0;
    int j = 0;
    DIR *dirp;
    struct dirent *dp;
    dirp = opendir("./");

    i += sprintf(buf + i, "\n");
    while ((dp = readdir(dirp)) != NULL)
    {
        if (i < BUFFER_SIZE - 256)
        {
            i += sprintf(buf + i, "%s    ", dp->d_name);
        }
        else
        {
            send(sockfd, buf, strlen(buf), 0);
            i = 0;
        }
    }

    send(sockfd, buf, strlen(buf), 0);
    (void)closedir(dirp);
}

/**
 * @fn        void data_packets_combining(FILE_INFO db, char *buf)
 * @brief     数据组包
 *
 * @param     [in] db            FILE_INFO 结构体，存储目标数据
 * @param     [in,out] buf       将 FILE_INFO 结构体中的数据组合，传入 buf
 *
 */
void data_packets_combining(FILE_INFO db, char *buf)
{
    sprintf(buf, "IP:%s;sockfd:%d;port:%d;filename:%s;filesize:%ld;",
            db.IP, db.sockfd, db.port, db.filename, db.filesize);
}

/**
 * @fn        void data_packets_parsing(FILE_INFO *db, char *buf)
 * @brief     数据包解析
 *
 * @param     [in,out] db        FILE_INFO 结构体，用于存储 解析自 buf 的数据
 * @param     [in] buf           存储信息的字符串
 *
 * @return    void
 */
void data_packets_parsing(FILE_INFO *db, char *buf)
{
    char *tmp;
    char t1[256];

    /* 解析 IP */
    tmp = strstr(buf, "IP:");
    tmp += 3;
    sscanf(tmp, "%[^;]", db->IP);

    /* 解析 sockfd */
    tmp = strstr(buf, "sockfd:");
    tmp += 7;
    sscanf(tmp, "%[^;]", t1);
    db->sockfd = atoi(t1);

    /* 解析 port */
    tmp = strstr(buf, "port:");
    tmp += 5;
    sscanf(tmp, "%[^;]", t1);
    db->port = atoi(t1);

    /* 解析 filename */
    tmp = strstr(buf, "filename:");
    tmp += 9;
    sscanf(tmp, "%[^;]", db->filename);

    /* 解析 filesize */
    tmp = strstr(buf, "filesize:");
    tmp += 9;
    sscanf(tmp, "%[^;]", t1);
    db->filesize = atoi(t1);
    return;
}

/**
 * @fn        void *file_transform(void *arg)
 * @brief     线程函数 - 传输文件
 *
 * @param     [in] arg       FILE_INFO 结构体，存放文件传输相关信息
 *
 * @return    void*
 */
void *file_transform(void *arg)
{
    file_transfor_flag = 1;

    FILE_INFO *db = (FILE_INFO *)arg;
    if (NULL == db)
    {
        DEBUG_INFO("入参 db 错误\n");
        file_transfor_flag = 0;
        close(db->sockfd);
    }

    FILE *transform_file_fd = NULL;

    char filebuf[BUFFER_SIZE + 1] = {'\0'};
    char md5_str[MD5_STR_LEN + 1] = {'\0'};

    clock_t t_start = 0;
    clock_t t_end = 0;

    int ret_send = -1;
    int ret_md5sum = -1;
    int file_block_length = 0;

    DEBUG_INFO("file_transform sockfd = %d\n", db->sockfd);

    while (1)
    {
        DEBUG_INFO("发送文件\n");
        /* 收到 客户端 $FILE_TRANSFER_START$回应，此时文件传输正式开始 */
        // if (FILE_TRANSFER_START == file_transfor_flag)
        if (file_transfor_flag)
        {
            /* 打开文件 开始传输 */
            DEBUG("开始传输\n");
            transform_file_fd = fopen(db->filename, "rb");
            if (NULL == transform_file_fd)
            {
                file_transfor_flag = FILE_TRANSFER_ERR;
                send(db->sockfd, "$FILE_ONT_FOUND$", sizeof("$FILE_ONT_FOUND$"), 0);
                close(db->sockfd);
                break;
            }
            DEBUG_INFO("db->filesize = %ld\n", db->filesize);
            fseek(transform_file_fd, db->filesize, SEEK_SET);

            t_start = clock();
            // while (file_transfer_state && (file_block_length = fread(filebuf, sizeof(char), BUFFER_SIZE, transform_file_fd)) > 0)
            while ((file_block_length = fread(filebuf, sizeof(char), BUFFER_SIZE, transform_file_fd)) > 0)
            {
                // DEBUG_INFO("file_transfer_state = %d\n", file_transfer_state);

                filebuf[BUFFER_SIZE] = '\0';
                ret_send = send(db->sockfd, filebuf, file_block_length, 0);
                if (ret_send <= 0)
                {
                    DEBUG_INFO("err : ret_send 错误\n");
                    break;
                }

                bzero(filebuf, sizeof(filebuf));

                if (file_block_length < BUFFER_SIZE)
                {
                    DEBUG_INFO("sleep\n");
                    usleep(200 * 1000);
                }

                if (!file_transfer_state)
                {
                    DEBUG_INFO("sleep\n");
                    usleep(200 * 1000);
                    break;
                }
            }

            t_end = clock();
            double time_totle = (double)(t_end - t_start) / CLOCKS_PER_SEC;

            if (file_transfer_state)
            {
                send(db->sockfd, "$FILE_TRANSFER_FINISHED$", sizeof("$FILE_TRANSFER_FINISHED$"), 0);
                DEBUG("文件: %s 传输成功 耗时 %.3f 秒\n", db->filename, time_totle);
                ret_md5sum = md5sum(db->filename, md5_str);
                if (0 == ret_md5sum)
                {
                    send(db->sockfd, md5_str, sizeof(md5_str), 0);
                    DEBUG("%s md5sum : %s\n", db->filename, md5_str);
                }
            }
            else
            {
                send(db->sockfd, "$FILE_TRANSFER_BREAK$", sizeof("$FILE_TRANSFER_BREAK$"), 0);
                DEBUG("文件: %s 传输中断 耗时 %.3f 秒\n", db->filename, time_totle);
            }

            fclose(transform_file_fd);
            close(db->sockfd);
            break;
        }
        else
        {
            break;
        }
    }
    DEBUG_INFO("file_transform 线程退出\n");
    // bzero(db,sizeof(FILE_INFO));
    pthread_exit(NULL);
}

/**
 * @fn        void *file_receive(void *arg)
 * @brief     线程函数 - 文件接收
 *
 * @param     [in] arg       FILE_INFO 结构体，存放文件传输相关信息
 *
 * @return    void*
 */
void *file_receive(void *arg)
{
    file_receive_flag = 1;
    FILE_INFO *db = (FILE_INFO *)arg;
    if (NULL == db)
    {
        DEBUG_INFO("入参 db 错误\n");
        file_receive_flag = 0;
    }

    char filebuf[BUFFER_SIZE + 1];
    bzero(filebuf, sizeof(filebuf));

    char md5_str[MD5_STR_LEN + 1] = {'\0'};
    DEBUG_INFO("file_receive sockfd = %d\n", db->sockfd);

    FILE *recv_file_fd = NULL;
    clock_t t_start;
    clock_t t_end;

    int ret_recv = -1;
    int ret_md5sum = -1;

    while (1)
    {
        DEBUG_INFO("接收文件准备\n");
        if (file_receive_flag)
        {
            DEBUG_INFO("接收文件\n");
            /* 打开文件 开始传输 */
            DEBUG("filename = %s\n", db->filename);

            /* 偏移量为 0 ，文件重新创建*/
            if (!db->filesize)
            {
                DEBUG_INFO("偏移量为 0\n");
                while (0 == access(db->filename, F_OK))
                {
                    // db->filename = 0;/***/
                    file_rename(db);
                    DEBUG_INFO("重命名\n");
                    DEBUG_INFO("filename = %s\n", db->filename);
                }
                recv_file_fd = fopen(db->filename, "wb");
            }
            else
            {
                DEBUG_INFO("断点续传\n");
                recv_file_fd = fopen(db->filename, "ab");
            }

            if (NULL == recv_file_fd)
            {
                file_transfor_flag = FILE_TRANSFER_ERR;
                DEBUG_INFO("文件: %s 打开失败\n", db->filename);
                break;
            }
            DEBUG_INFO("while (file_receive_flag)\n");
            DEBUG("文件传输中...\n");
            t_start = clock();
            while (1)
            {

                ret_recv = recv(db->sockfd, filebuf, BUFFER_SIZE, 0);

                if (ret_recv <= 0)
                {
                    DEBUG_INFO("err : recv return %d\n", ret_recv);
                    break;
                }

                if (!strncmp(filebuf, "$FILE_TRANSFER_FINISHED$", sizeof("$FILE_TRANSFER_FINISHED$")))
                {
                    break;
                }

                if (!strncmp(filebuf, "$FILE_TRANSFER_BREAK$", sizeof("$FILE_TRANSFER_BREAK$")))
                {
                    DEBUG_INFO("文件传输中断\n");
                    break;
                }

                if (!strncmp(filebuf, "$FILE_ONT_FOUND$", sizeof("$FILE_ONT_FOUND$")))
                {
                    DEBUG_INFO("服务器未找到 %s\n", db->filename);
                    fclose(recv_file_fd);
                    remove(db->filename);
                    DEBUG_INFO("remove \n");
                    goto FILE_ONT_FOUND;
                }

                // 此处 fwrite 第三参数填为 BUFFER_SIZE 会导致文件末尾出现多余的空格
                fwrite(filebuf, sizeof(char), ret_recv, recv_file_fd);
                bzero(filebuf, sizeof(filebuf));
            }

            t_end = clock();
            double time_totle = (double)(t_end - t_start) / CLOCKS_PER_SEC;
            if (file_transfer_state)
            {
                send(db->sockfd, "$FILE_RECV_FINISHED$", strlen("$FILE_RECV_FINISHED$"), 0);
                DEBUG("文件: %s 接收成功 耗时 %.3f 秒\n", db->filename, time_totle);
                recv(db->sockfd, filebuf, BUFFER_SIZE, 0);
            }
            else
            {
                send(db->sockfd, "$FILE_RECV_BREAK$", strlen("$FILE_RECV_BREAK$"), 0);
                DEBUG("文件: %s 接收中断 耗时 %.3f 秒\n", db->filename, time_totle);
            }

            fclose(recv_file_fd);

            ret_md5sum = md5sum(db->filename, md5_str);
            if (0 == ret_md5sum)
            {
                DEBUG("发送 %s MD5: %s\n", db->filename, filebuf);
                DEBUG("接收 %s MD5: %s\n", db->filename, md5_str);
                if (!strncmp(filebuf, md5_str, 32))
                {
                    DEBUG("MD5 校验成功！\n");
                }
                else
                {
                    DEBUG("MD5 校验失败！\n");
                }
            }

        FILE_ONT_FOUND:
            close(db->sockfd);
            break;
        }
        else
        {
            break;
        }
    }
    // bzero(db,sizeof(FILE_INFO));
    DEBUG_INFO("线程 file_receive 退出\n");
    pthread_exit(NULL);
}

/**
 * @fn        int socket_create(int port)
 * @brief     文件传输专用 socket 通道创建和专用端口绑定
 *
 * @param     [in] port      文件传输专用端口号
 *
 * @return    int            失败返回 -1; 成功返回 sockfd
 */
int socket_create(int port)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    DEBUG_INFO("socket_create sockfd = %d\n", sockfd);
    /* 绑定 bing */
    DEBUG("/* 绑定 bing */\n");
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /* 此处代码作用：关闭 socket 之后，端口资源立即释放而不是进入 TIME_WAIT */
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    if (-1 == bind(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)))
    {
        DEBUG_INFO("绑定失败\n");
        return -1;
    }

    if (-1 == listen(sockfd, 5))
    {
        DEBUG_INFO("监听失败\n");
        return -1;
    }
    // int c_sockfd = accept_client(sockfd);
    struct sockaddr_in client_addr;
    bzero(&client_addr, sizeof(struct sockaddr_in));
    socklen_t socket_len = sizeof(client_addr);
    int c_sockfd;
    while (1)
    {
        c_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &socket_len);
        if (c_sockfd != -1 && c_sockfd != 0)
            break;
    }
    DEBUG_INFO("socket_create sockfd = %d\n", sockfd);
    DEBUG_INFO("socket_create c_sockfd = %d\n", c_sockfd);
    DEBUG_INFO("文件传送线程 连接成功\n");
    close(sockfd);
    return c_sockfd;
}

/**
 * @fn        int c_sockfd(char *ip_addr, int port)
 * @brief
 *
 * @param     [in] ip_addr
 * @param     [in] port
 *
 * @return    int
 */
int c_sockfd(char *ip_addr, int port)
{
    DEBUG_INFO("IP:%s,port:%d\n", ip_addr, port);
    DEBUG_INFO("/* 创建 Socket */\n");
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in client_addr;
    bzero(&client_addr, sizeof(struct sockaddr_in));
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(port);
    client_addr.sin_addr.s_addr = inet_addr(ip_addr);

    int ret = connect(sockfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr));
    DEBUG_INFO("connect ret = %d\n", ret);
    DEBUG_INFO("连接服务器成功 sockfd = %d\n", sockfd);
    return sockfd;
}

/**
 * @fn        void display_data_packets(FILE_INFO db)
 * @brief     打印 FILE_INFO 结构体内容
 *
 * @param     [in] db        FILE_INFO 结构体
 *
 */
void display_data_packets(FILE_INFO db)
{
    DEBUG_INFO("\nIP:%s;sockfd:%d;port:%d;filename:%s;filesize:%ld;\n",
               db.IP, db.sockfd, db.port, db.filename, db.filesize);
}

/**
 * @fn        void file_rename(FILE_INFO *db)
 * @brief     文件重命名，传输文件时如果本地存在重名文件进行文件重命名
 *
 * @param     [in,out] db        FILE_INFO 结构体，其中存储了文件名
 *
 */
void file_rename(FILE_INFO *db)
{
    char file_extension[256];
    char file_prefix[256];
    char buf[256];
    char name[256];
    char *pos = NULL;
    char *pos1 = NULL;
    char *pos2 = NULL;

    int namelen = 0;

    pos = strrchr(db->filename, '.');

    if (NULL == pos || pos == db->filename)
    {
        strcpy(file_prefix, db->filename);
        sprintf(file_extension, "%c", '\0');
    }
    else
    {
        namelen = pos - db->filename;
        strncpy(file_prefix, db->filename, namelen);
        file_prefix[namelen] = '\0';
        strcpy(file_extension, pos + 1);
    }

    pos1 = strrchr(file_prefix, '(');
    pos2 = strrchr(file_prefix, ')');

    if ((NULL != pos1) && (NULL != pos2))
    {
        int num = 0;
        num = pos2 - pos1 - 1;
        strncpy(buf, pos1 + 1, num);
        buf[num] = '\0';

        strncpy(name, file_prefix, pos1 - file_prefix);
        name[pos1 - file_prefix] = '\0';
        num = atoi(buf);

        if (NULL == pos || pos == db->filename)
        {
            sprintf(db->filename, "%s(%d)", name, ++num);
        }
        else
        {
            sprintf(db->filename, "%s(%d).%s", name, ++num, file_extension);
        }
    }
    else
    {
        if (NULL == pos || pos == db->filename)
        {
            sprintf(db->filename, "%s(1)", file_prefix);
        }
        else
        {
            sprintf(db->filename, "%s(1).%s", file_prefix, file_extension);
        }
    }
}

/**
 * @fn        void file_info_bzero_except_ip(FILE_INFO *db)
 * @brief     FILE_INFO 结构体，除 IP 地址外，其余数据置零
 *
 * @param     [in,out] db
 *
 */
void file_info_bzero_except_ip(FILE_INFO *db)
{
    bzero(db->filename, sizeof(db->filename));
    db->filesize = 0;
    db->port = 0;
    db->sockfd = 0;
}